/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package pt.mleiria.supervisedlearning;

import pt.mleiria.utils.EnvSettings;
import pt.mleiria.utils.LogUtils;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import pt.mleiria.datastructures.matrixAlgebra.Matrix;

/**
 *
 * @author manuel
 */
public class KNearestNeighbour {

    protected final static Logger LOG = Logger.getLogger(KNearestNeighbour.class.getName());
    /**
     *
     */
    private Matrix group;
    /**
     *
     */
    private String[] labels;
    /**
     *
     */
    private int k;

    /**
     *
     * @param group
     * @param labels
     */
    public KNearestNeighbour(Matrix group, String[] labels) {
        this.group = group;
        this.labels = labels;
    }

    public KNearestNeighbour(final String dataFile, final String separator) {
        file2matrix(dataFile, separator);
    }

    private void file2matrix(final String dataFile, final String separator) {
        BufferedReader br = null;
        List<String> auxList = new ArrayList<String>();
        try {
            String currentLine;
            br = new BufferedReader(new FileReader(dataFile));
            while((currentLine = br.readLine()) != null){
                auxList.add(currentLine);
            }
        } catch (IOException e) {
            LOG.log(Level.SEVERE, e.getMessage());
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException ex) {
                LOG.log(Level.SEVERE, ex.getMessage());
            }
        }
        final int cols = auxList.get(0).split(separator).length - 1;
        final int rows = auxList.size();
        final double[][] components = new double[rows][cols];
        this.labels = new String[rows];
        for(int i = 0; i < auxList.size(); i++){
            final String[] line = auxList.get(i).split(separator);
            for(int j = 0; j < line.length-1; j++){
                components[i][j] = Double.parseDouble(line[j]);
                labels[i] = line[line.length-1];
            }
            
        }
        this.group = new Matrix(components);
        LOG.log(Level.INFO, "groups:\n{0}", group.toString());
        LOG.log(Level.INFO, "labels:\n{0}", LogUtils.showArrayContents(labels));
        this.group = this.group.autoNorm();
        LOG.log(Level.INFO, "groups:\n{0}", group.toString());
    }

    /**
     *
     * @param inX
     * @param k
     * @return
     */
    public String classify0(double[] inX, int k) {
        this.k = k;
        final Matrix inMX = tiles(inX, group.rows());
        LOG.log(Level.INFO, "inMX:\n{0}", inMX.toString());
        final Matrix sqDiffMat = inMX.subtract(group);
        LOG.log(Level.INFO, "sqDiffMat:\n{0}", sqDiffMat.toString());
        final Matrix sqDiffMatP2 = sqDiffMat.power(2);
        LOG.log(Level.INFO, "sqDiffMatP2:\n{0}", sqDiffMatP2.toString());
        int axis = 1;
        final Matrix sqDistances = sqDiffMatP2.sum(axis);
        LOG.log(Level.INFO, "sqDistances:\n{0}", sqDistances.toString());
        final Matrix distancesMatrix = sqDistances.power(0.5);
        LOG.log(Level.INFO, "distancesMatrix:\n{0}", distancesMatrix.toString());
        /*
         axis = 1 means a column Matrix
         */
        int size = distancesMatrix.rows();
        double[] distances = new double[size];
        for (int i = 0; i < size; i++) {
            distances[i] = distancesMatrix.component(i, 0);
        }

        return getNeighbours(distances);
    }

    private String getNeighbours(final double[] distances) {
        /*
         vai guardar os 3 valores mais baixos das distances
         */
        double[] result = new double[k];
        /*
         Vai guardar os respectivos indices para depois conseguir associar às labels
         */
        int[] indexes = new int[k];

        for (int i = 0; i < k; i++) {
            double min = distances[0];
            for (int j = 0; j < distances.length; j++) {
                if (distances[j] < min && distances[j] >= 0) {
                    min = distances[j];
                    indexes[i] = j;
                }
            }
            distances[indexes[i]] = -1;
            result[i] = min;
        }
        LOG.log(Level.INFO, "indexes:\n{0}", LogUtils.showArrayContents(indexes));
        LOG.log(Level.INFO, "result:\n{0}", LogUtils.showArrayContents(result));
        String majority = getMajority(indexes);
        LOG.log(Level.INFO, "MAJ:\n{0}", majority);
        return majority;
    }

    private String getMajority(final int[] indexes) {

        TreeMap<String, Integer> maj = new TreeMap<String, Integer>();
        for (int i = 0; i < indexes.length; i++) {
            if (maj.containsKey(labels[indexes[i]])) {
                maj.put(labels[indexes[i]], maj.get(labels[indexes[i]]) + 1);
            } else {
                maj.put(labels[indexes[i]], 1);
            }
        }
        return maj.lastKey();
    }

    private Matrix tiles(double[] inX, int size) {
        final int rows = size;
        final int cols = inX.length;
        final double[][] components = new double[rows][cols];
        for (int i = 0; i < cols; i++) {
            for (int j = 0; j < rows; j++) {
                components[j][i] = inX[i];
            }
        }
        return new Matrix(components);
    }

    public static void main(String[] args) {
        /*
        double[][] components = new double[4][2];
        components[0][0] = 1.0;
        components[0][1] = 1.1;
        components[1][0] = 1.0;
        components[1][1] = 1.0;
        components[2][0] = 0.0;
        components[2][1] = 0.0;
        components[3][0] = 0.0;
        components[3][1] = 0.1;
        final Matrix groups = new Matrix(components);
        LOG.log(Level.INFO, "Groups:\n{0}", groups.toString());
        double[] inX = new double[]{0.0, 0.0};
        String[] labels = new String[]{"A", "A", "B", "B"};
        KNearestNeighbour kNN = new KNearestNeighbour(groups, labels);
        kNN.classify0(inX, 3);
        */
        String datingTestSet = EnvSettings.DATING_TEST_SET;
        KNearestNeighbour kNN = new KNearestNeighbour(datingTestSet, "\t");
        
    }

}
